データ競合 と Rust
code:rs
pub struct BankAccount {
balance: i64,
}
code:rs
let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&mut account));
let _taker = std::thread::spawn(move || take_out(&mut account));
∵ 可変参照 は同じスコープで 1 つしか許されていないため コンパイルを通すためには、同期機構である Mutex を用いる 他の言語(e.g. C++ の std::mutex)とは異なり、独立したオブジェクトではなくラッパとなっている code:rs
pub struct BankAccount {
balance: std::sync::Mutex<i64>,
}
ロック を取得する Mutex::lock メソッドは Result<MutexGuard<T>, PoisonError<MutexGuard<T>>> を返す Result でラップされているのは、ロックが Poisoning となる可能性があるため Poisoning
スレッドが panic! を起こし、ロックが正常に解放されなかった状態
しかしほとんど起こり得ないのに加えて、起こったとしても即座にプログラムを停止した方が良いケースが多いので unwrap() とすることが多い
MutexGuard は Deref や DerefMut トレイトを実装しているため、内部値への プロキシ としての機能も持つ code:rs
pub fn balance(&self) -> i64 {
let balance = *self.balance.lock().unwrap();
if balance < 0 {
panic!("** Oh no, gone overdrawn: {}", balance);
}
balance
}
code:rs
pub fn deposit(&mut self, amount: i64) {
*self.balance.lock().unwrap() += amount
}
pub fn withdraw(&mut self, amount: i64) -> bool {
let mut balance = self.balance.lock().unwrap();
if *balance < amount {
return false;
}
*balance -= amount;
true
}
warning.icon
BankAccount の balance を変更するにも関わらず、&mut self ではなく &self を受け取る
借用チェッカ が事実上コンパイル時から実行時に移っているが、その代わりに スレッド 間の同期機構が追加される しかし、BankAccount の balance を Mutex でラップしても、生存期間 に関するエラーが発生する code:rs
let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&account));
let _taker = std::thread::spawn(move || take_out(&account));
∵ move により account が 1 つ目のクロージャにムーブし、クロージャの終了時にドロップされるため
code:rs
let account = std::sync::Arc::new(BankAccount::new());
account.deposit(1000);
let account2 = account.clone();
let _taker = std::thread::spawn(move || take_out(&account2));
let account3 = account.clone();
let _payer = std::thread::spawn(move || pay_in(&account3));
それぞれのスレッドは、参照カウンタ ポインタのコピーをクロージャにコピーして受け取る 参照されている BookAccount は参照カウントが 0 になった場合にのみ ドロップ される 以上を踏まえると、(unsafe コードを除く)「安全な」Rust では、データ競合 を完全に回避できる ことが分かる